/*===========================================================================
  Copyright (C) 2014 by the Okapi Framework contributors
-----------------------------------------------------------------------------
  This library is free software; you can redistribute it and/or modify it 
  under the terms of the GNU Lesser General Public License as published by 
  the Free Software Foundation; either version 2.1 of the License, or (at 
  your option) any later version.

  This library is distributed in the hope that it will be useful, but 
  WITHOUT ANY WARRANTY; without even the implied warranty of 
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License 
  along with this library; if not, write to the Free Software Foundation, 
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

  See also the full LGPL text here: http://www.gnu.org/copyleft/lesser.html
===========================================================================*/

package net.sf.okapi.lib.omegat;

import java.awt.Dialog;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Stack;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.sf.okapi.lib.xliff2.Util;
import net.sf.okapi.lib.xliff2.core.Tag;
import net.sf.okapi.lib.xliff2.core.CTag;
import net.sf.okapi.lib.xliff2.core.CloneFactory;
import net.sf.okapi.lib.xliff2.core.Fragment;
import net.sf.okapi.lib.xliff2.core.MTag;
import net.sf.okapi.lib.xliff2.core.PTag;
import net.sf.okapi.lib.xliff2.core.Part;
import net.sf.okapi.lib.xliff2.core.Part.GetTarget;
import net.sf.okapi.lib.xliff2.core.Segment;
import net.sf.okapi.lib.xliff2.core.TagType;
import net.sf.okapi.lib.xliff2.core.Tags;
import net.sf.okapi.lib.xliff2.core.TargetState;
import net.sf.okapi.lib.xliff2.core.Unit;
import net.sf.okapi.lib.xliff2.glossary.GlossEntry;
import net.sf.okapi.lib.xliff2.glossary.Glossary;
import net.sf.okapi.lib.xliff2.glossary.Translation;
import net.sf.okapi.lib.xliff2.its.TermTag;
import net.sf.okapi.lib.xliff2.reader.Event;
import net.sf.okapi.lib.xliff2.reader.XLIFFReader;
import net.sf.okapi.lib.xliff2.writer.XLIFFWriter;

import org.omegat.filters2.FilterContext;
import org.omegat.filters2.IAlignCallback;
import org.omegat.filters2.IParseCallback;
import org.omegat.filters2.ITranslateCallback;
import org.omegat.filters2.Instance;
import org.omegat.filters2.TranslationException;

public class XLIFF2Filter implements org.omegat.filters2.IFilter {

	private static final Logger LOGGER = Logger.getLogger(XLIFF2Filter.class.getName());
	private static final String EXTENSION = ".xlf";

	private static final Pattern patternLCOpening = Pattern.compile("\\<g(\\d+?)\\>");
	private static final Pattern patternLCClosing = Pattern.compile("\\</g(\\d+?)\\>");
	private static final Pattern patternLCMOpening = Pattern.compile("\\<m(\\d+?)\\>");
	private static final Pattern patternLCMClosing = Pattern.compile("\\</m(\\d+?)\\>");
	private static final Pattern patternLCIsolated = Pattern.compile("\\<x(\\d+?)/\\>");
	private static final Pattern patternLCProtected = Pattern.compile("\\<p(\\d+?)/\\>");

	private static final Pattern patternLCOpenCloseEncode = Pattern.compile("\\<(/?)(g+?\\d+?)\\>");
	private static final String replacementLCOpenCloseEncode = "<$1g$2>";
	private static final Pattern patternLCOpenCloseDecode = Pattern.compile("\\<(/)?g(g+?\\d+?)\\>");
	private static final String replacementLCOpenCloseDecode = "<$1$2>";

	private static final Pattern patternLCAllIsolatedEncode = Pattern.compile("\\<(([xbe])\\2*?\\d+?)/\\>");
	private static final String replacementLCAllIsolatedEncode = "<$2$1/>";
	private static final Pattern patternLCAllIsolatedDecode = Pattern.compile("\\<([xbe])(\\1+?\\d+?)/\\>");
	private static final String replacementLCAllIsolatedDecode = "<$2/>";
	
	protected IParseCallback parseCallback;
    protected ITranslateCallback translateCallback;
    protected IAlignCallback alignCallback;
    protected String defaultInEncoding = "UTF-8";
    protected String defaultOutEncoding = "UTF-8";
    protected String supportedExtensions;
    private Boolean processOmegaT2_5 = null;
    private boolean version3_plus = false;

	private String name;
	private XLIFFWriter writer;
	private PrintWriter glosWriter = null;
	private boolean triedGlossaryCreation = false;
	private File inputFile;
	private FilterContext filterContext;

	public XLIFF2Filter () {
		name = "XLIFF-2 files (Okapi)";
		supportedExtensions = EXTENSION;
	}

	@Override
	public void alignFile (File inFile,
		File outFile,
		Map<String, String> config,
		FilterContext context,
		IAlignCallback callback)
		throws Exception
    {
		// Do nothing
    	//throw new RuntimeException("Not implemented");
	}

//	@Override
//	public Map<String, String> changeOptions (Dialog parent,
//		Map<String, String> config)
//	{
//		return null;
//	}

	@Override
	public String getFileFormatName () {
		return name;
	}

	@Override
	public String getFuzzyMark () {
		return "fuzzy";
	}

	@Override
	public String getHint () {
		return "";
	}

	@Override
	public boolean hasOptions () {
		return false;
	}

	@Override
	public boolean isFileSupported (File inFile,
		Map<String, String> config,
		FilterContext context)
	{
		// Extension check is already done by OmegaT
		// Here we just need to check cases when an extension could be used for
		// several different filters (like .xliff)
		try {
			String version = XLIFFDetector.getXLIFFVersion(inFile);
			return version.equals("2.0");
		}
		catch ( Throwable e ) {
			return false;
		}
	}

	@Override
	public boolean isSourceEncodingVariable () {
		return false;
	}

	@Override
	public boolean isTargetEncodingVariable () {
		return true;
	}

	@Override
	public void parseFile (File inFile,
		Map<String, String> config,
		FilterContext context,
		IParseCallback callback)
		throws Exception
	{
		parseCallback = callback;
		translateCallback = null;
		alignCallback = null;
		checkOmegaTVersion();
        try {
            processFile(inFile, null, config, context);

            // 2.5 version also needs to link previous/next
            if ( requirePrevNextFields() ) {
                // parsing - need to link prev/next
                parseCallback.linkPrevNextSegments();
            }
            
        }
        finally {
        	parseCallback = null;
        }
	}

	@Override
	public void translateFile (File inFile,
		File outFile,
		Map<String, String> config,
		FilterContext context,
		ITranslateCallback callback)
		throws Exception
	{
		parseCallback = null;
		translateCallback = callback;
		alignCallback = null;
        try {
        	// For 2.5
        	if ( checkOmegaTVersion() ) {
        		translateCallback.setPass(1);
        	}
            processFile(inFile, outFile, config, context);

        	// For 2.5 only too
            if ( requirePrevNextFields() ) {
            	translateCallback.linkPrevNextSegments();
            	translateCallback.setPass(2);
                processFile(inFile, outFile, config, context);
            }
        }
        finally {
        	translateCallback = null;
        }
	}

    /**
     * Method can be overridden for return true. It means what two-pass parsing and translating will be
     * processed and previous/next segments will be linked.
     */
    protected boolean requirePrevNextFields () {
        return false; // Default: all Okapi filters have IDs
        // Take doProcessFor2_5() into account if this changes
    }
	
    protected void processFile (File inFile,
       	File outFile,
       	Map<String, String> config,
		FilterContext context)
    	throws IOException, TranslationException
    {
    	XLIFFReader reader = null;
    	writer = null;
    	inputFile = inFile;
    	filterContext = context;
    	
		try {
	    	// Get the source and target locales
	    	String srcLoc = "en";
	    	if ( context.getSourceLang() != null ) {
	    		srcLoc = context.getSourceLang().getLanguage();
	    	}
	    	String trgLoc = "fr";
	    	if ( context.getTargetLang() != null ) {
	    		trgLoc = context.getTargetLang().getLanguage();
	    	}

	    	// Get the source and target encoding
	    	String inEncoding = context.getInEncoding();
	    	if ( inEncoding == null ) { // auto
	    		inEncoding = defaultInEncoding;
	    	}
	    	String outEncoding = context.getOutEncoding();
	    	if ( outEncoding == null ) { // auto
	    		outEncoding = defaultOutEncoding;
	    	}

	    	// Process the document
			reader = new XLIFFReader();
			reader.open(inFile);
			if ( outFile != null ) {
				writer = new XLIFFWriter();
				writer.setUseIndentation(true);
				writer.setUseInsignificantParts(true);
				writer.create(outFile, srcLoc, trgLoc);
			}
			
			while ( reader.hasNext() ) {
				Event event = reader.next();
				if ( event.isUnit() ) {
					processUnit(event.getUnit());
				}
				if ( writer != null ) {
					// Write out the event
					writer.writeEvent(event);
				}
			}
		}
		finally {
			if ( reader != null ) reader.close();
			if ( writer != null ) writer.close();
        	// Close and reset the glossary if needed
        	if ( glosWriter != null ) {
        		glosWriter.close();
        		glosWriter = null;
        	}
        	triedGlossaryCreation = false;
		}
    	
    }
    
    private void processUnit (Unit unit) {

//    	unit.hideProtectedContent();
		int segCount = 0;
		
		for ( Segment seg : unit.getSegments() ) {

			String segId = "s"+segCount++;
			String srcInOF = toOmegat(seg, false);
			String trgInOF = null;
			boolean wasFuzzy = true;
			if ( seg.hasTarget() ) { // There is an existing translation
				trgInOF = toOmegat(seg, true);
				wasFuzzy = (( seg.getState()==TargetState.INITIAL )
					|| ( seg.getState()==TargetState.TRANSLATED ));
			}
			if ( writer == null ) { // Reading the document into OmegaT
				String comments = processComments(unit, seg);
				if ( seg.hasTarget()
					&& !seg.getTarget().equals(seg.getSource()) )
				{ // There is an existing translation
					parseCallback.addEntry(unit.getId()+"_"+segId,
						srcInOF, trgInOF, wasFuzzy, comments, this);
				}
				else { // No existing translation
					parseCallback.addEntry(unit.getId()+"_"+segId,
						srcInOF, null, false, comments, this);
				}
			}
			else { // Translation coming back from OmegaT
				String trans = translateCallback.getTranslation(unit.getId()+"_"+segId, srcInOF);
				// Put it back in the Okapi resource
				if ( Util.isNoE(trans) ) {
					// Empty translation from OmegaT: Use the source
					fromOmegat(srcInOF, seg, false);
				}
				else { // The target is updated
					fromOmegat(trans, seg, seg.hasTarget());
					if ( wasFuzzy ) seg.setState(TargetState.TRANSLATED);
				}
			}
		}
		processGlossary(unit);
		
//		unit.showProtectedContent();
    }
    
    private void processGlossary (Unit unit) {
    	if ( !unit.hasGlossEntry() ) return;
    	Glossary glossary = unit.getGlossary();
    	for ( GlossEntry entry : glossary ) {
        	String term = entry.getTerm().getText();
        	for ( Translation trans : entry ) {
    			writeToGlossary(term, trans.getText(), null);
        	}
    	}
    }

    protected void updateProtectedMarkers (Unit unit,
    	Tags srcMarkers,
    	Tags newMarkers)
    {
//    	for ( ProtectedCodedText pct : entries ) {
//			List<String> spans = pct.getSourceSpans();
//			int s = 0;
//			for ( String span : spans ) {
//				StringBuilder tmp = new StringBuilder();
//				for ( int i=0; i<span.length(); i++ ) {
//					switch ( span.charAt(i) ) {
//					case Fragment.CODE_OPENING:
//					case Fragment.CODE_CLOSING:
//					case Fragment.CODE_PLACEHOLDER:
//					case Fragment.ANNO_OPENING:
//					case Fragment.ANNO_CLOSING:
//						newMarkers.add(srcMarkers.get(Fragment.toIndex(span.charAt(++i))));
//						tmp.append(""+span.charAt(i-1)+Fragment.toChar(newMarkers.size()-1));
//						break;
//					case Fragment.SPECIAL_PROTECTED:
//						// Should not occurs
//						throw new RuntimeException("Unexpected special-protected marker.");
//					default:
//						tmp.append(span.charAt(i));
//						break;
//					}
//				}
//				spans.set(s, tmp.toString());
//				s++;
//			}
//    	}
    }
    
//    private void PREVIOUS_processUnit (Unit unit) {
//		// Skip non-translatable entries
//		if ( !unit.getTranslate() ) return; // TODO: FIx to take overides into account
//		
//		Markers newMarkers = new Markers(unit.getStore());
//		int segCount = 0;
//
//		for ( Segment seg : unit.getSegments() ) {
//			// Segment defaults
//			boolean isFuzzy = false;
//			Fragment trgFrag = null;
//
//			segCount++;
//			String segId = "s"+segCount; // TODO: Use real id if possible
//			
//			// Get the possible target
//			if ( seg.hasTarget() ) {
//				trgFrag = seg.getTarget();
//				switch ( seg.getState() ) {
//				case INITIAL:
//				case TRANSLATED:
//					isFuzzy = true;
//					break;
//				case REVIEWED:
//				case FINAL:
//					break;
//				}
//			}
//			
//			if ( writer == null ) { // Reading the document
//				// Populate OmegaT
//				//String comments = processComments(tu, srcSeg, trgSeg);
//				if (( trgFrag == null ) || trgFrag.isEmpty() ) {
//					// No existing translation
//					parseCallback.addEntry(
//						unit.getId()+"_"+segId,
//						toOmegat(seg.getSource()),
//						null,
//						false, null, this);
//				}
//				else {
//					// There is an existing translation
//					parseCallback.addEntry(
//						unit.getId()+"_"+segId,
//						toOmegat(seg.getSource()),
//						toOmegat(trgFrag),
//						isFuzzy, null, this);
//				}
//			}
//			else { // Translation coming back from OmegaT
//				String trans = translateCallback.getTranslation(
//					unit.getId()+"_"+segId, toOmegat(seg.getSource()));
//				// Put it back in the Okapi resource
//				if ( Util.isEmpty(trans) ) {
//					 // Empty translation from OmegaT
//					seg.setTarget(new Fragment(seg.getStore(), true)); 
//				}
//				else { // Create the new target content
//					fromOmegat(trans, seg, newMarkers);
//				}
//			}
//		}
//		
//		if ( writer != null ) {
//			// Set the new set of target tags
//			unit.getStore().getTargetMarkers().reset(newMarkers);
//		}
//    }
    
    protected String processComments (Unit tu,
    	Segment segment)
    {
    	if ( !segment.getSource().hasTag() ) return null;
    	
    	Fragment srcFrag = segment.getSource();
    	String ct = srcFrag.getCodedText();
		StringBuilder tmp = new StringBuilder();
    	for ( int i=0; i<ct.length(); i++ ) {
    		if ( Fragment.isChar1(ct.charAt(i)) ) {
    			Tag tag = srcFrag.getTags().get(ct, i); i++;
    			if ( !tag.isMarker() ) continue;
    			if ( tag.getTagType() == TagType.CLOSING ) continue;
    			
    			MTag mtag = (MTag)tag;
    			if ( mtag.getType().equals("comment") ) {
    				String value = mtag.getValue();
    				if ( value != null ) tmp.append(value);
    			}

    			if ( mtag instanceof TermTag ) {
    				TermTag ttag = (TermTag)mtag;
    				if ( tmp.length() > 0 ) tmp.append("\n");
    				String term = getSpan(ct, i+1, ttag, srcFrag);
    				tmp.append("Term: \'"+term+"'");
    				String info = ttag.getValue();
    				if ( info != null ) tmp.append(" "+info);
    				Double conf = ttag.getTermConfidence();
    				if ( conf != null ) tmp.append(" Confidence="+conf);
    				writeToGlossary(term, null, info);
    			}
//
//    			// Text Analysis
//    			ann = anns.getFirstAnnotation(GenericAnnotationType.TA);
//    			if ( ann != null ) {
//    				if ( tmp.length() > 0 ) tmp.append("\n");
//    				String term = getSpan(ct, i+1, c, tf);
//    				tmp.append("TA: \'"+term+"'");
//    				StringBuilder info = new StringBuilder();
//    				String str = ann.getString(GenericAnnotationType.TA_CLASS);
//    				if ( str != null ) info.append(" Class:"+str);
//    				str = ann.getString(GenericAnnotationType.TA_IDENT);
//    				if ( str != null ) info.append(" Ident:"+str);
//    				str = ann.getString(GenericAnnotationType.TA_SOURCE);
//    				if ( str != null ) info.append(" Src:"+str);
//    				Double conf = ann.getDouble(GenericAnnotationType.TERM_CONFIDENCE);
//    				if ( conf != null ) info.append(" Confidence="+Util.formatDouble(conf));
//    				tmp.append(info);
//    				writeToGlossary(term, info.toString());
//    			}
//
//    			// Localization Quality Issue
//    			//TODO
    		}
    	}
    	return (( tmp.length() == 0 ) ? null : tmp.toString() );
    }
	
    /**
     * Gets the text-only span of content for a given open/close code.
     * @param ct the coded text to lookup.
     * @param start the start index in the coded text.
     * @param code the start code. 
     * @param tf the text fragment corresponding to the coded text.
     * @return the span of content (stripped of it's inline codes) that is between
     * the given start code and its ending code.
     */
    private String getSpan (String ct,
    	int start,
    	Tag opening,
    	Fragment frag)
    {
    	// Get the index of the closing code
    	int index = frag.getClosingPosition(opening);
    	// If none: return empty span
    	if ( index == -1 ) return "";
    	String span = frag.getCodedText().substring(start, index);
		Matcher m = Fragment.TAGREF_REGEX.matcher(span);
		return m.replaceAll("");
    }
    
    /**
     * Generates a OmegaT coded string from a given Fragment.
     * For now we use g and x codes.
     * TODO: At some point we should try to follow OmegaT's XLIFFDialect.constructShortcuts()
     * @param part the part to convert
     * @param target true to process the target, false to process the source.
     * @return the OmegaT string
     */
	protected String toOmegat (Part part,
		boolean target)
	{
		String ct = target ? part.getTarget().getCodedText() : part.getSource().getCodedText();
		Tags tags = target ? part.getTargetTags() : part.getSourceTags();
		
		StringBuilder tmp = new StringBuilder();
		boolean encodeExistingLetterCodes = true;
		if ( encodeExistingLetterCodes ) {
			Matcher m = patternLCOpenCloseEncode.matcher(ct);
			ct = m.replaceAll(replacementLCOpenCloseEncode);
			m = patternLCAllIsolatedEncode.matcher(ct);
			ct = m.replaceAll(replacementLCAllIsolatedEncode);
		}

		int nidCount = -1;
		Stack<SimpleEntry<String, Integer>> stack = new Stack<>();
		String id;
		int nid;
		//int pmNidCount = -1;
		
		for ( int i=0; i<ct.length(); i++ ) {
			switch ( ct.charAt(i) ) {
			case Fragment.CODE_OPENING:
				stack.push(new SimpleEntry<String, Integer>(tags.get(ct, i).getId(), ++nidCount)); i++;
				tmp.append(String.format("<g%d>", nidCount));
				break;
			case Fragment.CODE_CLOSING:
				id = tags.get(ct, i).getId(); i++;
				nid = -1; // Search for the corresponding numeric id
				// We use a loop rather than push/pop to allow for overlapping codes
				for ( int j=0; j<stack.size(); j++ ) {
					if ( stack.get(j).getKey().equals(id) ) {
						nid = stack.get(j).getValue();
						stack.remove(j);
						break;
					}
				}
				if ( nid == -1 ) { // Isolated case probably
					nid = ++nidCount;
				}
				tmp.append(String.format("</g%d>", nid));
				break;
			case Fragment.CODE_STANDALONE:
				// No need to use the stack for standalone codes
				tmp.append(String.format("<x%d/>", ++nidCount));
				i++;
				break;
			case Fragment.MARKER_OPENING:
			case Fragment.MARKER_CLOSING:
				// Strip out the annotation markers
				i++;
				break;
			case Fragment.PCONT_STANDALONE:
				// No need to use the stack for standalone codes
				tmp.append(String.format("<p%d/>", ++nidCount));
				i++;
				break;
			default:
				tmp.append(ct.charAt(i));
				break;
			}
		}
		return tmp.toString();
	}

	protected void fromOmegat (String text,
		Part part,
		boolean target)
	{
		boolean decodeEncodedLetterCodes = true;		
		Fragment trgFrag = part.getTarget(GetTarget.CREATE_EMPTY);
		
		// Case with no in-line codes
		if ( text.indexOf('<') == -1 ) {
			trgFrag.setCodedText(text);
			return;
		}
		// Otherwise: we have in-line codes
		
		Tags srcTags = part.getSourceTags();
		Tags trgTags = part.getTargetTags();
		StringBuilder tmp = new StringBuilder(text);
		
		// Re-construct the list of numeric ids based on the order in the fragment
		// map = <id used in OmegaT string , id of the tag>
		HashMap<String, String> idMap = new HashMap<String, String>();
		// Create the map using which ever source/target was used to go to OmegaT format
		String ct = target ? part.getTarget().getCodedText() : part.getSource().getCodedText();
		Tags tags = target ? part.getTargetTags() : part.getSourceTags();
		int nidCount = -1;
		for ( int i=0; i<ct.length(); i++ ) {
			switch ( ct.charAt(i) ) {
			case Fragment.CODE_OPENING:
			// Stripped out case Fragment.OPENING_ANNOTATION:
				idMap.put(Integer.toString(++nidCount), tags.get(ct, i).getId()); i++;
				break;
			case Fragment.CODE_STANDALONE:
				idMap.put(Integer.toString(++nidCount), tags.get(ct, i).getId()); i++;
				break;
			case Fragment.CODE_CLOSING:
			// Stripped out case Fragment.CLOSING_ANNOTATION:
				String id = tags.get(ct, i).getId(); i++;
				if ( !idMap.containsValue(id) ) {
					idMap.put(Integer.toString(++nidCount), id);
				}
				break;
			case Fragment.PCONT_STANDALONE:
				// use the reference key for the protected code
				int key = Fragment.toKey(ct.charAt(i), ct.charAt(++i));
				idMap.put(Integer.toString(++nidCount), ""+key);
				break;
			case Fragment.MARKER_OPENING:
			case Fragment.MARKER_CLOSING:
				break;
			}
		}
		
		int n;
		int start = 0;
		int diff = 0;
		Tag code;
		int key;
		
		Matcher m = patternLCOpening.matcher(text);
		while ( m.find(start) ) {
			n = m.start();
			code = srcTags.get(idMap.get(m.group(1)), TagType.OPENING);
			if ( code == null ) {
				code = new CTag(TagType.OPENING, m.group(1), tmp.substring(n+diff, (n+diff)+m.group().length()));
			}
			key = trgTags.add(code);
			tmp.replace(n+diff, (n+diff)+m.group().length(), Fragment.toRef(key));
			diff += (2-m.group().length());
			start = n+m.group().length();
		}
		
		start = diff = 0;
		m = patternLCClosing.matcher(tmp.toString());
		while ( m.find(start) ) {
			n = m.start();
			code = srcTags.get(idMap.get(m.group(1)), TagType.CLOSING);
			if ( code == null ) {
				code = new CTag(TagType.CLOSING, m.group(1), tmp.substring(n+diff, (n+diff)+m.group().length()));
			}
			key = trgTags.add(code);
			tmp.replace(n+diff, (n+diff)+m.group().length(), Fragment.toRef(key));
			diff += (2-m.group().length());
			start = n+m.group().length();
		}
		
		start = diff = 0;
		m = patternLCIsolated.matcher(tmp.toString());
		while ( m.find(start) ) {
			n = m.start();
			code = srcTags.get(idMap.get(m.group(1)), TagType.STANDALONE);
			if ( code == null ) {
				code = new CTag(TagType.STANDALONE, m.group(1), tmp.substring(n+diff, (n+diff)+m.group().length()));
			}
			key = trgTags.add(code);
			tmp.replace(n+diff, (n+diff)+m.group().length(), Fragment.toRef(key));
			diff += (2-m.group().length());
			start = n+m.group().length();
		}

		start = diff = 0;
		m = patternLCMOpening.matcher(tmp.toString());
		while ( m.find(start) ) {
			n = m.start();
			code = srcTags.get(idMap.get(m.group(1)), TagType.OPENING);
			if ( code == null ) {
				code = new MTag(m.group(1), MTag.TYPE_DEFAULT);
			}
			key = trgTags.add(code);
			tmp.replace(n+diff, (n+diff)+m.group().length(), Fragment.toRef(key));
			diff += (2-m.group().length());
			start = n+m.group().length();
		}
		
		start = diff = 0;
		m = patternLCMClosing.matcher(tmp.toString());
		while ( m.find(start) ) {
			n = m.start();
			code = srcTags.get(idMap.get(m.group(1)), TagType.CLOSING);
			if ( code == null ) {
				MTag opening = trgTags.getOpeningMTag(m.group(1));
				code = new MTag(opening);
			}
			key = trgTags.add(code);
			tmp.replace(n+diff, (n+diff)+m.group().length(), Fragment.toRef(key));
			diff += (2-m.group().length());
			start = n+m.group().length();
		}
		
		// Change protected text last because it can have inline codes 
		// and those would not be in the OmegaT string
		start = diff = 0;
		m = patternLCProtected.matcher(tmp.toString());
		while ( m.find(start) ) {
			n = m.start();
			PTag pm = srcTags.getPTag(Integer.valueOf(idMap.get(m.group(1))));
			// Add the hidden tags (the old will be removed when calling trgFrag.clear())
			StringBuilder pmtmp = new StringBuilder(pm.getCodedText());
			for ( int i=0; i<pmtmp.length(); i++ ) {
				char pmch = pmtmp.charAt(i);
				if ( Fragment.isChar1(pmch) ) {
					Tag bm = srcTags.get(pmtmp, i);
					key = trgTags.add(CloneFactory.create(bm, trgTags));
					pmtmp.replace(i, i+2, Fragment.toRef(key));
					i++;
				}
			}
			// Place the reference to the PMarker
			PTag newpm = new PTag();
			newpm.setCodedText(pmtmp.toString());
			key = trgTags.add(pm);
			tmp.replace(n+diff, (n+diff)+m.group().length(), Fragment.toRef(key));
			diff += (2-m.group().length());
			start = n+m.group().length();
		}

		String codedText = tmp.toString();

		if (decodeEncodedLetterCodes) {
			m = patternLCOpenCloseDecode.matcher(codedText);
			codedText = m.replaceAll(replacementLCOpenCloseDecode);
			m = patternLCAllIsolatedDecode.matcher(codedText);
			codedText = m.replaceAll(replacementLCAllIsolatedDecode);
		}

		// Remove text and tags of the previous translation (if any)
		trgFrag.clear();
		// Set the coded text for the next content.
		// The tags are already in the tags object for that store.
		trgFrag.setCodedText(codedText);
	}
	
	private boolean checkOmegaTVersion () {
		if ( processOmegaT2_5 == null ) {
			try {
				String tmp = ResourceBundle.getBundle("org/omegat/Version").getString("version");
				processOmegaT2_5 = (tmp.compareTo("2.5.0") >= 0);
				version3_plus = (tmp.compareTo("3.0.0") >= 0);
				LOGGER.info("Omtv= "+tmp+" flag2_5="+processOmegaT2_5+" flag3_plus="+version3_plus);
			}
			catch ( Throwable e ) {
				processOmegaT2_5 = false;
				version3_plus = false;
			}
		}
		return processOmegaT2_5;
	}

	private void writeToGlossary (String term,
		String trans,
		String info)
	{
		if ( !triedGlossaryCreation && version3_plus ) {
			triedGlossaryCreation = true;
			try {
				String glosRoot = filterContext.getProjectProperties().getGlossaryRoot();
				File file = new File(new File(glosRoot), inputFile.getName()+".utf8");
				LOGGER.info("Creating glossary "+file.getPath());
				glosWriter = net.sf.okapi.common.Util.charsetPrintWriter(file.getAbsolutePath(), StandardCharsets.UTF_8);
			}
			catch ( Throwable e) {
				// Cannot create glossary file
				LOGGER.severe("Failed to create glossary for "+inputFile.getPath()+"\n"+e.getMessage());
			}
		}
		if ( glosWriter != null ) {
			glosWriter.println(String.format("%s\t%s\t%s", term,
				((trans == null) ? "" : trans),
				((info == null) ? "" : info)));
		}
	}

	@Override
	public Instance[] getDefaultInstances () {
        return new Instance[] {
        	new Instance("*.xlf")
        };
	}

	@Override
	public Map<String, String> changeOptions (Dialog parent,
		Map<String, String> config) {
		// No options for now
		return null;
	}

}
